home *** CD-ROM | disk | FTP | other *** search
- #
- # This file is part of OpenVIP (http://openvip.sourceforge.net)
- #
- # Copyright (C) 2002-2003
- # Michal Dvorak, Jiri Sedlar, Antonin Slavik, Vaclav Slavik, Jozef Smizansky
- #
- # This program is licensed under GNU General Public License version 2;
- # see file COPYING in the top level directory for details.
- #
- # $Id: TimelineWidget.py,v 1.57 2003/06/08 10:42:48 vaclavslavik Exp $
- #
- # Widget that displays data represented by model.Timeline
- #
-
- import os.path
- import model, globals, openvip, notify
- import worker, threading
- from wxPython.wx import *
-
- TRACK_HEIGHT = 32
- RULER_HEIGHT = 20
- BACKGROUND_COLOR = wxColour(0xFF,0xFF,0xDD)
- HEADER_COLOR = wxColour(0xD9,0xDC,0xF6)
- HEADER_WIDTH = 50
- VSCROLL_STEP = TRACK_HEIGHT
- HSCROLL_STEP = VSCROLL_STEP
-
- SNAP_TOLERANCE = 3 #pixels
- RESIZE_TOLERANCE = 3 #pixels
-
- # Number of tracks displayed:
- NUM_TRACKS = 5
-
- STD_VIDEO_TRACKS = ['VA', 'VFx', 'VB'] + \
- [('V%i' % i) for i in range(0,NUM_TRACKS)]
- STD_AUDIO_TRACKS = ['AA', 'AFx', 'AB'] + \
- [('A%i' % i) for i in range(0,NUM_TRACKS)]
-
- THUMBNAIL_HEIGHT = TRACK_HEIGHT - 3
-
- ADDITIONAL_SPACE_ON_RIGHT = 0.25
-
- # widget modes:
- MODE_FAST = 1
- MODE_ONE_THUMBNAIL = 2
- MODE_THUMBNAILS = 3
-
- # mouse action modes:
-
- # Mode for selecting, dragging and resizing objects:
- MOUSE_MODE_NORMAL = 1
- # Mode for cutting objects into pieces:
- MOUSE_MODE_CUT = 2
- # Mode for adding new objects:
- MOUSE_MODE_ADD = 3
-
-
- if sys.platform == 'win32': fontFace = 'Arial'
- else: fontFace = ''
- fontNormal = wxFont(8, wxDEFAULT, wxNORMAL, wxNORMAL, faceName=fontFace)
- fontTiny = wxFont(7, wxDEFAULT, wxNORMAL, wxNORMAL, faceName=fontFace)
-
- def sec2timecode(sec):
- """Converts seconds to timecode in form H:MM:SS."""
- h = int(sec / 3600)
- m = int((sec - h * 3600) / 60)
- s = int(sec - h * 3600 - m * 60)
- return '%i:%02i:%02i' % (h,m,s)
-
- wxEVT_UPDATE_THUMBNAILS = wxNewEventType()
-
- def EVT_UPDATE_THUMBNAILS(win, func):
- win.Connect(-1, -1, wxEVT_UPDATE_THUMBNAILS, func)
-
- class UpdateThumbnailsEvent(wxPyEvent):
- """This event is generated when new thumbnail is available."""
- def __init__(self):
- wxPyEvent.__init__(self)
- self.SetEventType(wxEVT_UPDATE_THUMBNAILS)
-
-
- # maximum number of thumbnails kept in memory - the cache is purged
- # when exceeded:
- MAX_THUMBS = 200
-
- # number of thumbnails in cache:
- thumbsCnt = 0
-
-
- class Thumbnail:
- """Helper class that represents generated thumbnail for a clip."""
-
- NOTHING = 0
- MAKING = 1
- IMAGE = 2
- BITMAP = 3
-
- class Generator:
- """This is a function that generates thumbnails on background"""
- def __init__(self, thumb, obj, mode, zoom, windows):
- self.thumb = thumb
- self.obj = obj
- self.mode = mode
- self.zoom = zoom
- self.index = 0
- self.bitmaps = []
- self.windows = windows
-
- def __notify(self):
- for wnd in self.windows:
- evt = UpdateThumbnailsEvent()
- evt.SetEventObject(wnd)
- wxPostEvent(wnd, evt)
-
- def run(self):
- file = self.obj.src_spec['filename']
- try:
- fi = globals.get_file_info(file)
- except openvip.Error:
- wxLogError("Can't get information about file '%s'" % file)
- return
- for s in fi.video_streams:
- if s.name == self.obj.src_channel:
- self.thumb.size = (s.width * THUMBNAIL_HEIGHT / s.height,
- THUMBNAIL_HEIGHT)
- pixels=int((self.obj.time_to-self.obj.time_from)*self.zoom)
- if self.mode == MODE_ONE_THUMBNAIL:
- frames = 3
- self.thumb.count = 1
- else:
- frames = (pixels / self.thumb.size[0]) + 1
- self.thumb.count = frames
- self.bitmaps = [ (Thumbnail.NOTHING,None)
- for i in range(0,frames) ]
- self.thumb.bitmaps = self.bitmaps
- self.g = globals.core.create_thumbnails_generator(file,
- s.name, self.thumb.size[0], self.thumb.size[1],
- frames, openvip.DestCallback(None))
- self.thumb.ready = True
- self.__notify()
- return
- for s in fi.audio_streams:
- if s.name == self.obj.src_channel:
- self.thumb.size = (THUMBNAIL_HEIGHT, THUMBNAIL_HEIGHT)
- pixels=int((self.obj.time_to-self.obj.time_from)*self.zoom)
- if self.mode == MODE_THUMBNAILS:
- frames = (pixels / self.thumb.size[0]) + 1
- self.thumb.size = (pixels/frames, THUMBNAIL_HEIGHT)
- self.bitmaps = [ (Thumbnail.NOTHING,None)
- for i in range(0,frames) ]
- self.thumb.bitmaps = self.bitmaps
- self.g = globals.core.create_thumbnails_generator(file,
- s.name,
- self.thumb.size[0], self.thumb.size[1],
- frames, openvip.DestCallback(None))
- self.thumb.count = frames
- self.thumb.ready = True
- self.__notify()
- return
-
- def makeBitmap(self, index):
- global thumbsCnt
- thumbsCnt += 1
- # NB: we can't create wxBitmap in non-main thread, so we just
- # create wxImage (this is MT-safe) and convert it later
- data = self.g.render_single_frame(index)
- img = wxEmptyImage(self.thumb.size[0], self.thumb.size[1])
- img.SetData(data)
- self.thumb.lock.acquire()
- if index < len(self.bitmaps):
- self.bitmaps[index] = (Thumbnail.IMAGE,img)
- self.thumb.lock.release()
- if self.mode == MODE_THUMBNAILS or self.mode == MODE_ONE_THUMBNAIL:
- self.__notify()
-
-
- def __init__(self, obj, mode, zoom, wnds):
- self.lock = threading.Lock()
- self.bitmaps = []
- self.size = None
- self.count = 0
- self.ready = False
- self.generator = Thumbnail.Generator(self, obj, mode, zoom, wnds)
- worker.enqueue(self.generator.run)
- #self.generator.run()
-
- def getBmp(self, index):
- """Return wxBitmap with given index or None if not available yet."""
- if not self.ready:
- return None
- self.lock.acquire()
- flag, data = self.bitmaps[index]
- self.lock.release()
- if flag == Thumbnail.NOTHING:
- self.bitmaps[index] = (Thumbnail.MAKING,None)
- worker.enqueue(self.generator.makeBitmap, index)
- #self.generator.makeBitmap(index)
- return None
- elif flag == Thumbnail.MAKING:
- return None
- elif flag == Thumbnail.IMAGE:
- data = wxBitmapFromImage(data)
- self.bitmaps[index] = (Thumbnail.BITMAP,data)
- return data
-
-
- class TrackHeader(wxWindow):
- """List of tracks displayed on the left of timeline widget.
- Implementation detail of TimelineWidget"""
- def __init__(self, parent, widget, tracks, vscroll):
- self.vscroll = vscroll
- self.widget = widget
- self.tracks = tracks
- wxWindow.__init__(self, parent, -1, size=(HEADER_WIDTH,-1))
- self.SetBackgroundColour(HEADER_COLOR)
- EVT_PAINT(self, self.OnPaint)
-
- def OnPaint(self, event):
- size = self.GetSize()
- dc = wxPaintDC(self)
- dc.SetFont(fontNormal)
- dc.SetDeviceOrigin(0, -self.vscroll.GetThumbPosition()*VSCROLL_STEP)
- y = 0
- shift = (TRACK_HEIGHT-dc.GetCharHeight())/2
- for i in self.tracks:
- dc.DrawText(i, 5, y+shift)
- y += TRACK_HEIGHT
- dc.DrawLine(0, y, size.x, y)
-
-
- class Ruler(wxWindow):
- """'Ruler' object on top of timeline widget.
- Implementation detail of TimelineWidget"""
- def __init__(self, widget):
- self.widget = widget
- self.selecting = False
- wxWindow.__init__(self, widget, -1, size=(1, RULER_HEIGHT),
- style=wxNO_FULL_REPAINT_ON_RESIZE)
-
- EVT_PAINT(self, self.OnPaint)
- EVT_LEFT_DOWN(self, self.OnLeftDown)
- EVT_LEFT_UP(self, self.OnLeftUp)
- EVT_MOTION(self, self.OnMouseMove)
-
- def OnNotify(self):
- self.Refresh()
-
- def OnPaint(self, event):
- size = self.GetSize()
- zoom = self.widget.zoom
- origin = self.widget.videopart.origin
- dc = wxPaintDC(self)
- dc.SetFont(fontTiny)
- dc.SetDeviceOrigin(origin[0], 0)
- x = -origin[0]
- t = 0
- cnt = 0
- step = 0.1; bigstep = 1
- if step*zoom < 10: step = 1; bigstep = 10
- if step*zoom < 10: step = 10; bigstep = 60
- if step*zoom < 10: step = 60; bigstep = 5*60
-
- while x < size.x - origin[0]:
- x = int(t * zoom)
- if x >= -origin[0]:
- if (int(t*100) % (100*bigstep) == 0) or cnt == 0:
- dc.DrawLine(x, size.y/3, x, size.y-1)
- dc.DrawText(sec2timecode(t), x, 0)
- cnt = 0
- else:
- dc.DrawLine(x, 2*size.y/3-1, x, size.y-1)
- t += step
- cnt += 1
-
- if self.widget.timeSelection != None:
- dc.SetLogicalFunction(wxINVERT)
- x1 = self.widget.timeSelection[0] * self.widget.zoom
- x2 = self.widget.timeSelection[1] * self.widget.zoom
- dc.DrawRectangle(x1, 0, x2-x1+1, size.y)
- dc.SetLogicalFunction(wxCOPY)
-
- def OnLeftDown(self, event):
- self.selecting = True
- self.selFrom = event.GetX() - self.widget.videopart.origin[0]
- self.selTo = event.GetX() - self.widget.videopart.origin[0]
-
- def OnMouseMove(self, event):
- if self.selecting:
- self.selTo = event.GetX() - self.widget.videopart.origin[0]
- else:
- event.Skip()
-
- def OnLeftUp(self, event):
- if self.selecting:
- if self.selTo > self.selFrom:
- x1 = self.selFrom
- x2 = self.selTo
- else:
- x1 = self.selTo
- x2 = self.selFrom
- z = self.widget.zoom
- if x2 - x1 >= 2:
- self.widget.timeSelection = (x1/z, x2/z)
- else:
- self.widget.timeSelection = None
- self.widget.NotifyWatchers()
- self.selecting = False
-
- # Current position indicator:
- self.widget.SetPosition(float(event.GetX() -
- self.widget.videopart.origin[0]) / self.widget.zoom)
- self.widget.NotifyWatchers()
-
- class DraggingInfo:
- pass
-
- CURSOR_CUT = 'CURSOR_CUT'
- CURSOR_ADD = 'CURSOR_ADD'
- CURSOR_CANT_ADD = 'CURSOR_CANT_ADD'
- class Part(wxWindow):
- """The main part timeline widget, where objects are shown. Part is actually
- half of the widget: either video-only (top) or audio-only (bottom)
- part of it. It handles all user input.
- Implementation detail of TimelineWidget"""
- bmpArrowUp = None
- bmpArrowDown = None
-
- def __init__(self, parent, widget, tracks, vscroll):
- if Part.bmpArrowUp == None:
- Part.bmpArrowUp = wxBitmap('bitmaps/arrow_up.png')
- Part.bmpArrowDown = wxBitmap('bitmaps/arrow_down.png')
- Part.cursors = {
- wxCURSOR_ARROW: wxStockCursor(wxCURSOR_ARROW),
- wxCURSOR_SIZEWE: wxStockCursor(wxCURSOR_SIZEWE),
- CURSOR_CUT: widget.cursorCut,
- CURSOR_ADD: widget.cursorAdd,
- CURSOR_CANT_ADD: widget.cursorCantAdd,
- }
- self.origin = (0,0)
- self.cursor = wxCURSOR_ARROW
- self.mouseMoved = None
- self.vscroll = vscroll
- self.widget = widget
- self.tracks = tracks
- self.backbuffer = None
- self.dragging = None
- self.resizing = None
- wxWindow.__init__(self, parent, -1, style=wxNO_FULL_REPAINT_ON_RESIZE)
- self.SetFont(fontNormal)
- self.SetBackgroundColour(BACKGROUND_COLOR)
- self.objBrush = wxBrush(wxWHITE, wxSOLID)
- self.AdjustVScrollbar()
-
- EVT_PAINT(self, self.OnPaint)
- EVT_UPDATE_THUMBNAILS(self, self.OnUpdateThumbnails)
- EVT_SIZE(self, self.OnSize)
- EVT_IDLE(self, self.OnIdle)
- EVT_LEFT_DOWN(self, self.OnLeftDown)
- EVT_LEFT_UP(self, self.OnLeftUp)
- EVT_RIGHT_DOWN(self, self.OnRightDown)
- EVT_MOTION(self, self.OnMouseMove)
- EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)
-
- def OnEraseBackground(self, event):
- pass
-
- def OnUpdateThumbnails(self, event):
- self.Refresh(False)
-
- def OnNotify(self):
- self.Refresh(False)
-
- def OnSize(self, event):
- self.backbuffer = None
- self.AdjustVScrollbar()
- event.Skip(True)
-
- def CalcObjsPos(self, objects=None, transitions=None):
- self.objPos = {}
- y = 0
- if objects == None: objects = self.widget.model.objects
- if transitions == None: transitions = self.widget.model.transitions
- def calcPos(obj, y, zoom):
- tfrom = int(obj.time_from * zoom)
- tto = int(obj.time_to * zoom)
- return (tfrom, y, tto - tfrom + 1, TRACK_HEIGHT+1)
- for i in self.tracks:
- for obj in [o for o in objects if o.track==i]:
- self.objPos[obj] = calcPos(obj, y, self.widget.zoom)
- if i == 'VFx' or i == 'AFx':
- for obj in [o for o in transitions if o.track==i]:
- self.objPos[obj] = calcPos(obj, y, self.widget.zoom)
- y += TRACK_HEIGHT
-
- def CalcOrigin(self):
- self.origin = (-self.widget.hscroll.GetThumbPosition()*HSCROLL_STEP,
- -self.vscroll.GetThumbPosition()*VSCROLL_STEP)
-
- def OnPaint(self, event):
- mod = self.widget.model
- size = self.GetSize()
- dcw = wxPaintDC(self)
- if mod == None or (size.x <= 0 or size.y <= 0):
- return;
-
- if thumbsCnt > MAX_THUMBS:
- self.widget.CreateThumbnails()
-
- dc = wxMemoryDC()
- if self.backbuffer == None:
- self.backbuffer = wxEmptyBitmap(size.x, size.y)
- dc.SelectObject(self.backbuffer)
- dc.SetBackground(wxBrush(BACKGROUND_COLOR, wxSOLID))
- dc.Clear()
- dc.SetDeviceOrigin(self.origin[0], self.origin[1])
-
- y = 0
- shift = (TRACK_HEIGHT-dc.GetCharHeight())/2
- dc.SetBrush(self.objBrush)
- dc.SetFont(fontNormal)
- for i in self.tracks:
- y += TRACK_HEIGHT
- dc.DrawLine(-self.origin[0], y, -self.origin[0] + size.x, y)
-
- # draw objects:
- for o in self.objPos:
- x,y,w,h = self.objPos[o]
- dc.DrawRectangle(x, y, w, h)
-
- # --- transitions:
- if isinstance(o, model.Transition):
- ext = dc.GetTextExtent(o.id)
- if o.direction == 'AB': bmp = Part.bmpArrowDown
- else: bmp = Part.bmpArrowUp
- bw = 4 + bmp.GetWidth()
- dc.DrawBitmap(bmp, x+2, y+2)
- dc.DrawText(o.id, x+bw+(w-bw-ext[0])/2, y+(h-ext[1])/2)
-
- # --- objects (clips):
- else:
- wd = self.widget
- if wd.mode != MODE_FAST:
- try:
- bmpCnt = wd.thumbnails[o].count
- except KeyError:
- bmpCnt = 0
- filename = os.path.basename(o.src_spec['filename'])
-
- if wd.mode == MODE_FAST or bmpCnt == 0:
- ext = dc.GetTextExtent(filename)
- dc.DrawText(filename, x + (w-ext[0])/2, y + (h-ext[1])/2)
-
- elif wd.mode == MODE_ONE_THUMBNAIL or bmpCnt == 1:
- if wd.thumbnails[o].ready:
- ext = dc.GetTextExtent(filename)
- xofs = 4+wd.thumbnails[o].size[0]
- dc.DrawText(filename, x + xofs + (w-xofs-ext[0])/2,
- y + (h-ext[1])/2)
- bmp=wd.thumbnails[o].getBmp(0)
- if bmp != None: dc.DrawBitmap(bmp, x+2, y+2)
-
- elif wd.mode == MODE_THUMBNAILS:
- if wd.thumbnails[o].ready:
- x2=x+2
- sz = wd.thumbnails[o].size[0]
- inc = float(w-4-sz)/(bmpCnt-1)
- p = 0
- for bi in range(0, bmpCnt):
- if x2+sz >= -self.origin[0]:
- if x2 > -self.origin[0] + size.x: break
- b = wd.thumbnails[o].getBmp(bi)
- if b != None: dc.DrawBitmap(b, x2, y+2)
- p += 1
- x2 = x+2 + p*inc
-
- # draw selections:
- dc.SetPen(wxTRANSPARENT_PEN)
- dc.SetLogicalFunction(wxINVERT)
- for o in self.widget.selection:
- try:
- x,y,w,h = self.objPos[o]
- dc.DrawRectangle(x, y, w, h)
- except KeyError: pass
- if self.widget.timeSelection != None:
- x1 = self.widget.timeSelection[0] * self.widget.zoom
- x2 = self.widget.timeSelection[1] * self.widget.zoom
- dc.DrawRectangle(x1, 0, x2-x1+1, size.y)
- dc.SetLogicalFunction(wxCOPY)
-
- # draw current position indicator:
- if self.widget.position != None:
- x = int(self.widget.position * self.widget.zoom)
- dc.SetPen(wxRED_PEN)
- dc.DrawLine(x, -self.origin[1], x, -self.origin[1] + size.y)
-
- # blit to real dc:
- dc.SetDeviceOrigin(0, 0)
- dcw.Blit(0, 0, size.x, size.y, dc, 0, 0)
- dc = None
-
- def FindObjectAt(self, x, y):
- x -= self.origin[0]
- y -= self.origin[1]
- for obj in self.objPos:
- l,t,w,h = self.objPos[obj]
- if x>=l and x<l+w and y>=t and y<t+h:
- return obj
- return None
-
- def OffsetTrack(self, track, dy, recoverFromError=True):
- try:
- index = self.tracks.index(track) + dy
- except ValueError:
- return track
- if index < 0:
- if recoverFromError: index = 0
- else: return None
- if index >= len(self.tracks):
- if recoverFromError: index = len(self.tracks)-1
- else: return None
- return self.tracks[index]
-
- def SnapObjects(self, objects, resizeable=False):
- """Ajust positions of given objects so that they are aligned with
- the rest of objects nearby."""
- for o in objects:
- if o not in self.objPos.keys(): continue
- pf, y, w, h = self.objPos[o]
- pt = pf + w - 1
- for o2 in self.objPos.keys():
- if o2 in objects: continue
- pf2, y, w, h = self.objPos[o2]
- pt2 = pf2 + w - 1
- if resizeable:
- if abs(pf - pf2) <= SNAP_TOLERANCE:
- o.time_from = o2.time_from
- break
- elif abs(pf - pt2) <= SNAP_TOLERANCE:
- o.time_from = o2.time_to
- break
- elif abs(pt - pt2) <= SNAP_TOLERANCE:
- o.time_to = o2.time_to
- break
- elif abs(pt - pf2) <= SNAP_TOLERANCE:
- o.time_to = o2.time_from
- break
- else:
- if abs(pf - pf2) <= SNAP_TOLERANCE:
- o.move(o2.time_from)
- break
- elif abs(pf - pt2) <= SNAP_TOLERANCE:
- o.move(o2.time_to)
- break
- elif abs(pt - pt2) <= SNAP_TOLERANCE:
- o.move(o2.time_to - o.length())
- break
- elif abs(pt - pf2) <= SNAP_TOLERANCE:
- o.move(o2.time_from - o.length())
- break
-
-
- def AdjustDrag(self, objects, dx, dy):
- old_dx = dx
- old_dy = dy
- changing = True
- iter = 10 # prevent
- while changing and iter > 0:
- iter -= 1
- changing = False
- dy = old_dy
- for o in objects:
- dfrom = o.time_from + dx
- dto = o.time_to + dx
- # check for out-of-timeline case:
- if dfrom < 0:
- dx = -o.time_from
- changing = True
- continue
- if dy != 0:
- track = self.OffsetTrack(o.track, dy, False)
- if track == None:
- return None
- else:
- track = o.track
-
- # check for colisitions with other objects:
- obstacles = [x for x in self.widget.model.objects if
- (x.track == track and x not in objects)]
- for o2 in obstacles:
- if not (dto <= o2.time_from or dfrom >= o2.time_to):
- # snap to left/right:
- left = abs(dto - o2.time_from)
- right = abs(dfrom - o2.time_to)
- if right > left:
- dx = o2.time_from - o.length() - o.time_from
- else:
- dx = o2.time_to - o.time_from
- if o.time_from + dx < 0:
- return None # out of timeline, wa can't do anything
- dy = 0 # only first object moves vertically
-
-
- if iter == 0: return None # conflict & failed to adjust
- if old_dx == dx: return 0
- return dx - old_dx
-
- def UpdateCursor(self, x, y):
- selobj = None
- if self.widget.mouseMode == MOUSE_MODE_CUT:
- cursor = CURSOR_CUT
- elif self.widget.mouseMode == MOUSE_MODE_ADD:
- time = float(x-self.origin[0])/self.widget.zoom
- yy = y/TRACK_HEIGHT
- if yy >= len(self.tracks): yy = len(self.tracks)-1
- if yy < 0: yy = 0
- track = self.tracks[yy]
- if self.widget.addCallback.veto(time, track):
- cursor = CURSOR_CANT_ADD
- else:
- cursor = CURSOR_ADD
- else:
- x -= self.origin[0]
- y -= self.origin[1]
- cursor = wxCURSOR_ARROW
- for obj in self.objPos:
- l,t,w,h = self.objPos[obj]
- r = l+w
- if y>=t and y<t+h and obj.track[1:3] == 'Fx':
- if ((x >= l-RESIZE_TOLERANCE and x<= l+RESIZE_TOLERANCE) or
- (x >= r-RESIZE_TOLERANCE and x<= r+RESIZE_TOLERANCE)):
- cursor = wxCURSOR_SIZEWE
- selobj = obj
- break
- if cursor != self.cursor:
- self.cursor = cursor
- self.SetCursor(Part.cursors[cursor])
- return selobj
-
- def RefreshCursor(self):
- pt = self.ScreenToClient(wxGetMousePosition())
- self.UpdateCursor(pt.x, pt.y)
-
- def OnIdle(self, event):
- if self.mouseMoved != None:
- self.UpdateCursor(self.mouseMoved[0], self.mouseMoved[1])
- self.mouseMoved = None
- event.Skip()
-
- def __FindGroup(self, objs):
- """Returns objects from all groups containing any objects from objs."""
- d = {}
- m = self.widget.model
- for o in objs:
- d[o] = o
- # iterate all groups that contain this object
- for g in [x for x in m.groups if o in x.objects]:
- for o2 in g.objects:
- if o2 not in objs: d[o2] = o2
- return d.values()
-
- def __BeginDragging(self, mainObj, objs, mouseX, mouseY):
- self.dragging = DraggingInfo()
- self.dragging.objs = [mainObj] + \
- [x for x in self.__FindGroup(objs) if x != mainObj]
- self.dragging.from_pos = (mouseX, mouseY/TRACK_HEIGHT)
- self.dragging.to_pos = self.dragging.from_pos
- primary = self.dragging.objs[0].track[0]
- self.dragging.allowY = len([x for x in self.dragging.objs if x.track[0] == primary]) == 1
- if self.dragging.allowY and self.dragging.objs[0].track[1:3] == 'Fx':
- self.dragging.allowY = False
- self.CaptureMouse()
-
- def OnLeftDown(self, event):
- if self.widget.model == None: return
- if self.widget.mouseMode != MOUSE_MODE_NORMAL: return
- if event.ControlDown(): return
-
- # Begin transition resizing:
- if self.cursor == wxCURSOR_SIZEWE:
- obj = self.UpdateCursor(event.GetX(), event.GetY())
- if obj != None:
- p = event.GetX()-self.origin[0]
- l = self.objPos[obj][0]
- r = l + self.objPos[obj][2]
- if abs(l-p) < abs(r-p): side=0
- else: side=1
- self.resizing = (obj, side)
- self.CaptureMouse()
- return
-
- # Begin dragging:
- obj = self.FindObjectAt(event.GetX(), event.GetY())
- if obj not in self.widget.selection: return
- self.__BeginDragging(obj, self.widget.selection,
- event.GetX(), event.GetY())
-
- def OnRightDown(self, event):
- if self.widget.model == None:
- event.Skip()
- return
- elif self.widget.mouseMode == MOUSE_MODE_CUT:
- self.widget.CancelCut()
- elif self.widget.mouseMode == MOUSE_MODE_ADD:
- self.widget.CancelAddObject()
- else:
- event.Skip()
-
- def OnMouseMove(self, event):
- if self.widget.model == None: return
- # Resizing transition:
- if self.resizing != None:
- obj, side = self.resizing
- pos = (event.GetX()-self.origin[0])/self.widget.zoom
- if side == 0: obj.time_from = pos
- else: obj.time_to = pos
- if obj.time_from > obj.time_to:
- x = obj.time_from
- obj.time_from = obj.time_to
- obj.time_to = x
- self.resizing = (self.resizing[0], 1-self.resizing[1])
- self.CalcObjsPos(transitions=[obj])
- self.Refresh(eraseBackground=False)
- return
-
- # Nothing:
- if self.dragging == None:
- self.mouseMoved = (event.GetX(), event.GetY())
- event.Skip()
- return
-
- # Dragging:
- prev = self.dragging.to_pos
- if not self.dragging.allowY:
- self.dragging.to_pos = (event.GetX(), self.dragging.from_pos[1])
- else:
- y = event.GetY()/TRACK_HEIGHT
- if y >= len(self.tracks): y = len(self.tracks)-1
- if self.tracks[y][1:3] == 'Fx': y = self.dragging.to_pos[1]
- self.dragging.to_pos = (event.GetX(), y)
-
- dx = self.dragging.to_pos[0]-self.dragging.from_pos[0]
- dy = (self.dragging.to_pos[1]-self.dragging.from_pos[1])*TRACK_HEIGHT
-
- adjust = self.AdjustDrag(self.dragging.objs,
- float(dx)/self.widget.zoom,
- dy/TRACK_HEIGHT)
- if adjust == None:
- self.dragging.to_pos = prev
- return
- if adjust != 0:
- self.dragging.to_pos = (self.dragging.to_pos[0] +
- int(adjust*self.widget.zoom),
- self.dragging.to_pos[1])
- dx += int(adjust*self.widget.zoom)
-
- if prev == self.dragging.to_pos:
- return
-
- self.widget.videopart.__PaintDraggedObjs(self.dragging, prev, dx, dy)
- self.widget.audiopart.__PaintDraggedObjs(self.dragging, prev, dx, dy)
-
- def __PaintDraggedObjs(self, dragging, prev, dx, dy):
- dc = wxClientDC(self)
- dc.SetDeviceOrigin(self.origin[0], self.origin[1])
- dc.SetLogicalFunction(wxINVERT)
- dc.SetPen(wxTRANSPARENT_PEN)
- dpx = prev[0]-dragging.from_pos[0]
- dpy = (prev[1]-dragging.from_pos[1])*TRACK_HEIGHT
- for obj in dragging.objs:
- try:
- x,y,w,h = self.objPos[obj]
- dc.DrawRectangle(x+dx, y+dy, w, h)
- dc.DrawRectangle(x+dpx, y+dpy, w, h)
- except KeyError: pass
- dy = dpy = 0 # only first object moves vertically
-
- def OnLeftUp(self, event):
- if self.widget.model == None: return
-
- # CUT mode:
- if self.widget.mouseMode == MOUSE_MODE_CUT:
- time = float(event.GetX()-self.origin[0])/self.widget.zoom
- self.widget._doCut(time)
- return
-
- # ADD mode:
- if self.widget.mouseMode == MOUSE_MODE_ADD:
- time = float(event.GetX()-self.origin[0])/self.widget.zoom
- y = event.GetY()/TRACK_HEIGHT
- if y >= len(self.tracks): y = len(self.tracks)-1
- track = self.tracks[y]
- if not self.widget.addCallback.veto(time, track):
- self.widget._endAddMode(time, track)
- return
-
- # transition is being resized:
- if self.resizing != None:
- obj = self.resizing[0]
- self.resizing = None
- self.ReleaseMouse()
- self.CalcObjsPos()
- self.SnapObjects([obj], resizeable=True)
- notify.notify(self.widget.model)
- return
-
- # objects are being dragged:
- if self.dragging != None:
- self.ReleaseMouse()
- if self.dragging.from_pos != self.dragging.to_pos:
- dx = self.dragging.to_pos[0]-self.dragging.from_pos[0]
- dy = self.dragging.to_pos[1]-self.dragging.from_pos[1]
- if dx != 0:
- for obj in self.dragging.objs:
- p = obj.time_from + dx/self.widget.zoom
- if p < 0: p = 0
- obj.move(p)
- self.CalcObjsPos()
- self.SnapObjects(self.widget.selection)
- notify.notify(self.widget.model)
- if dy != 0 and self.dragging.allowY:
- obj = self.widget.selection[0]
- obj.track = self.OffsetTrack(obj.track, dy)
- notify.notify(self.widget.model)
- self.dragging = None
- return
-
- # object selection:
- obj = self.FindObjectAt(event.GetX(), event.GetY())
- if obj == None:
- sel = []
- else:
- if event.ControlDown():
- if obj not in self.widget.selection:
- sel = self.widget.selection + [obj]
- else:
- sel = self.widget.selection[:] # make copy
- sel.remove(obj)
- else:
- sel = [obj]
-
- # Current position indicator:
- self.widget.SetPosition(float(event.GetX() - self.origin[0]) / self.widget.zoom)
- if not self.widget.SetSelection(sel):
- self.widget.NotifyWatchers()
-
- def AdjustVScrollbar(self):
- scroll = self.vscroll
- pos = scroll.GetThumbPosition()
- totsize = (len(self.tracks) * TRACK_HEIGHT) / VSCROLL_STEP
- thumbsize = self.GetSize()[1] / VSCROLL_STEP
- scroll.SetScrollbar(pos, thumbsize, totsize, thumbsize, refresh=True)
-
-
- class TimelineWidget(wxPanel):
- """TimelineWidget is central part of OpenVIP GUI. It contains two active
- areas for manipulating video tracks and audio tracks, header with names
- of tracks and ruler that indicates current scrolled position and zoom
- level.
- User can do following actions: select, move, destroy objects, resize
- transitions, select groups of objects and move them, set position in
- time, select time interval."""
-
- def __init__(self, parent, id, size, objectPanel, previewframe=None):
- img = wxImage('bitmaps/cut_cursor.cur')
- self.cursorCut = wxCursorFromImage(img)
- img = wxImage('bitmaps/add_cursor.cur')
- self.cursorAdd = wxCursorFromImage(img)
- img = wxImage('bitmaps/cant_add_cursor.cur')
- self.cursorCantAdd = wxCursorFromImage(img)
-
- self.mouseMode = MOUSE_MODE_NORMAL
- self.objectPanel = objectPanel
- self.previewframe = previewframe
- self.model = None
- self.audiotracks = STD_AUDIO_TRACKS
- self.videotracks = STD_VIDEO_TRACKS
- self.selection = None
- self.timeSelection = None
- self.position = None
-
- self.zoom = wxConfigBase_Get().ReadFloat('/TimelineWidget/zoom', 1.0)
-
- wxPanel.__init__(self, parent, id, size=size)
- self.ruler = Ruler(self)
- self.splitter = \
- wxSplitterWindow(self, -1,
- style=wxSP_3D|wxSP_BORDER)
- self.video = wxPanel(self.splitter, -1)
- self.audio = wxPanel(self.splitter, -1)
- self.vvscroll = wxScrollBar(self.video, -1, style=wxSB_VERTICAL)
- self.avscroll = wxScrollBar(self.audio, -1, style=wxSB_VERTICAL)
- self.videohdr = TrackHeader(self.video, self, self.videotracks,
- self.vvscroll)
- self.videopart = Part(self.video, self, self.videotracks, self.vvscroll)
- self.audiohdr = TrackHeader(self.audio, self, self.audiotracks,
- self.avscroll)
- self.audiopart = Part(self.audio, self, self.audiotracks, self.avscroll)
-
- sizer = wxBoxSizer(wxHORIZONTAL)
- sizer.Add(self.videohdr, 0, wxEXPAND)
- sizer.Add(self.videopart, 1, wxEXPAND)
- sizer.Add(self.vvscroll, 0, wxEXPAND)
- self.video.SetSizer(sizer)
- self.video.SetAutoLayout(True)
- sizer = wxBoxSizer(wxHORIZONTAL)
- sizer.Add(self.audiohdr, 0, wxEXPAND)
- sizer.Add(self.audiopart, 1, wxEXPAND)
- sizer.Add(self.avscroll, 0, wxEXPAND)
- self.audio.SetSizer(sizer)
- self.audio.SetAutoLayout(True)
-
- self.splitter.SplitHorizontally(self.video, self.audio,
- self.GetSize().y/2)
- self.splitter.SetMinimumPaneSize(20)
- self.hscroll = wxScrollBar(self, -1, style=wxSB_HORIZONTAL)
- if sys.platform == 'unix':
- # workaround a bug in wxGTK
- self.vvscroll.SetSize(self.vvscroll.GetBestSize())
- self.avscroll.SetSize(self.avscroll.GetBestSize())
- self.hscroll.SetSize(self.hscroll.GetBestSize())
-
- sizer = wxBoxSizer(wxVERTICAL)
- hsizer = wxBoxSizer(wxHORIZONTAL)
- hsizer.Add(HEADER_WIDTH, 1)
- hsizer.Add(self.ruler, 1)
- hsizer.Add(self.avscroll.GetSize().x, 1)
- sizer.Add(hsizer, 0, wxEXPAND)
- sizer.Add(self.splitter, 1, wxEXPAND)
- hsizer = wxBoxSizer(wxHORIZONTAL)
- hsizer.Add(HEADER_WIDTH, 1)
- hsizer.Add(self.hscroll, 1)
- hsizer.Add(self.avscroll.GetSize().x, 1)
- sizer.Add(hsizer, 0, wxEXPAND)
-
- self.SetAutoLayout(True)
- self.SetSizer(sizer)
- self.Layout()
- self.AdjustHScrollbar()
-
- self.watchers = [self.videopart.OnNotify,
- self.audiopart.OnNotify,
- self.ruler.OnNotify]
-
- EVT_SIZE(self, self.OnSize)
- EVT_SCROLL(self.vvscroll, self.OnScroll)
- EVT_SCROLL(self.avscroll, self.OnScroll)
- EVT_SCROLL(self.hscroll, self.OnScroll)
-
- mode = wxConfigBase_Get().ReadInt('/TimelineWidget/thumb_mode',
- MODE_ONE_THUMBNAIL)
- self.SetMode(mode)
-
- def OnScroll(self, event):
- o = event.GetEventObject()
- if o == self.vvscroll:
- self.videopart.Refresh(eraseBackground=False)
- self.videohdr.Refresh()
- self.videopart.CalcOrigin()
- elif o == self.avscroll:
- self.audiopart.Refresh(eraseBackground=False)
- self.audiohdr.Refresh()
- self.audiopart.CalcOrigin()
- elif o == self.hscroll:
- self.videopart.Refresh(eraseBackground=False)
- self.audiopart.Refresh(eraseBackground=False)
- self.videopart.CalcOrigin()
- self.audiopart.CalcOrigin()
- self.ruler.Refresh()
-
- def OnSize(self, event):
- self.splitter.SetSashPosition(event.GetSize().y/2, True)
- self.AdjustHScrollbar()
- event.Skip(True)
-
- def AdjustHScrollbar(self):
- if self.model == None: return
- width = self.videopart.GetSize()[0]
- totwidth = self.model.length()*self.zoom
- if totwidth < width: totwidth = width
- totwidth += width * ADDITIONAL_SPACE_ON_RIGHT
- scroll = self.hscroll
- pos = scroll.GetThumbPosition()
- totsize = totwidth / HSCROLL_STEP
- thumbsize = width / HSCROLL_STEP
- scroll.SetScrollbar(pos, thumbsize, totsize, thumbsize, refresh=True)
-
- def SetModel(self, m):
- """Must be called to set the model that the widget works on."""
- if self.model != None:
- notify.unlisten(self.model, self.OnModelChanged)
- self.model = m
- notify.listen(m, self.OnModelChanged)
- self.selection = []
- self.timeSelection = None
- self.OnModelChanged()
- self.CreateThumbnails()
- if self.previewframe != None:
- self.previewframe.SetModel(self.model)
-
- def SetZoom(self, zoom):
- """Set zoom level. 'zoom' is float number that is interpreted as
- number of pixels (horizontal) used to display 1 second
- of timeline."""
- center = float(self.hscroll.GetThumbPosition()*HSCROLL_STEP +
- self.GetClientWidth()/2)/self.zoom
- self.zoom = zoom
- pos = int(center * self.zoom - self.GetClientWidth()/2)
- self.OnModelChanged()
- self.CreateThumbnails()
- self.hscroll.SetThumbPosition(pos / HSCROLL_STEP)
- # synthetize event, SetThumbPosition doesn't send it:
- e=wxScrollEvent()
- e.SetEventObject(self.hscroll)
- self.OnScroll(e)
- wxConfigBase_Get().WriteFloat('/TimelineWidget/zoom', self.zoom)
-
- def SetMode(self, mode):
- """Set thumbnails visualization mode. One of
- - MODE_FAST (don't show thumbnails)
- - MODE_ONE_THUMBNAIL (show only single thumbnail and only video)
- - MODE_THUMBNAILS (show full filmstrip and audio waveforms)"""
- self.mode = mode
- self.CreateThumbnails()
- self.NotifyWatchers()
- wxConfigBase_Get().WriteInt('/TimelineWidget/thumb_mode', mode)
-
- def SetSelection(self, sel):
- """Sets selection. Selection is list of objects or transitions."""
- if self.selection != sel:
- self.selection = sel
- self.NotifyWatchers()
- if self.objectPanel != None:
- if len(sel) == 1:
- self.objectPanel.SetObject(self.model, sel[0])
- else:
- self.objectPanel.SetObject(self.model, None)
- return True
- return False
-
- def GetClientWidth(self):
- """Returns width of client area (Part) in pixels."""
- return self.videopart.GetSize().x
-
- def GetThumbnailHeight(self):
- """Returns height of thumbnails."""
- return THUMBNAIL_HEIGHT
-
- def GetPosition(self):
- """Returns position of (in seconds) of current position indicator.
- May be None."""
- return self.position
-
- def GetTimeSelection(self):
- """Returns selected time interval (as a tuple, from and to time in
- seconds or None if no time selection was made by the user."""
- return self.timeSelection
-
- def SetPosition(self, pos):
- """Sets position in time. See also GetPosition."""
- self.position = pos
- if self.previewframe!=None:
- self.previewframe.SetPosition(self.position)
-
- def Cut(self, callback=None):
- """Switches TimelineWidget into cutting mode. In this mode, clicking
- causes objects to be cut into two pieces. After the cut operation
- finishes, 'callback' is called."""
- self.mouseMode = MOUSE_MODE_CUT
- self.videopart.RefreshCursor()
- self.audiopart.RefreshCursor()
- self.cutCallback = callback
-
- def CancelCut(self):
- """Cancels cut mode. May only be called after Cut()."""
- self.mouseMode = MOUSE_MODE_NORMAL
- self.videopart.RefreshCursor()
- self.audiopart.RefreshCursor()
-
- def _doCut(self, time):
- if self.selection == None:
- self.model.cut(time)
- else:
- self.model.cut(time, self.selection)
- notify.notify(self.model)
- if self.cutCallback != None:
- self.cutCallback()
- self.CreateThumbnails()
-
- def AddObject(self, callback):
- """Adds object to timeline. 'callback' is class instance that has
- two methods:
- add(time, track) - called when mouse button clicked on position
- 'time' (in seconds) on track 'track'
- veto(time, track) - called when mouse moves, returns True if
- no object can be placed here, False if it
- can be placed.
- Note that mouse cursor does not identify *exact* position of new
- object. For instance, if the user clicks on VFx track anywhere
- where two video clips overlap, a transition that covers full extent
- of the overlap will be inserted.
-
- "To add" means that TimelineWidget switches to ADD mode in which
- special cursor is displayed. TimelineWidget
- does not respond as in normal mode. Instead, when some area
- is clicked, callback.add is called and if mouse move, callback.veto
- is called."""
- self.mouseMode = MOUSE_MODE_ADD
- self.addCallback = callback
- self.videopart.RefreshCursor()
- self.audiopart.RefreshCursor()
-
- def CancelAddObject(self):
- """Cancels object adding mode. May only be called after AddObject()."""
- self.mouseMode = MOUSE_MODE_NORMAL
- self.videopart.RefreshCursor()
- self.audiopart.RefreshCursor()
- self.addCallback = None
-
- def _endAddMode(self, time, track):
- self.mouseMode = MOUSE_MODE_NORMAL
- self.videopart.RefreshCursor()
- self.audiopart.RefreshCursor()
- self.addCallback.add(time, track)
- self.addCallback = None
-
- def OnModelChanged(self):
- if self.selection != None:
- allobj = self.model.objects + self.model.transitions
- self.selection = [x for x in self.selection if x in allobj]
- self.videopart.CalcObjsPos()
- self.audiopart.CalcObjsPos()
- self.AdjustHScrollbar()
- self.NotifyWatchers()
- if self.objectPanel != None:
- if len(self.selection) == 1:
- self.objectPanel.SetObject(self.model, self.selection[0])
- else:
- self.objectPanel.SetObject(self.model, None)
-
- def NotifyWatchers(self):
- for func in self.watchers: func()
-
- def CreateThumbnails(self):
- self.thumbnails = {}
- global thumbsCnt
- thumbsCnt = 0
- if self.mode == MODE_FAST or self.model == None: return
- for o in self.model.objects:
- self.thumbnails[o] = Thumbnail(o, self.mode, self.zoom,
- [self.videopart, self.audiopart])
-
-
-
-
- # ---------------- testing code -----------------
- if __name__ == '__main__':
- class TestFrame(wxFrame):
- def __init__(self):
- wxFrame.__init__(self, NULL, -1, "TimelineWidget Test")
- win = TimelineWidget(self, -1, size=(700,400), objectPanel=None)
- win.SetModel(model.load('test.timeline'))
- sizer = wxBoxSizer(wxVERTICAL)
- sizer.Add(win, 1, wxEXPAND)
- self.SetAutoLayout(True)
- self.SetSizer(sizer)
- self.Fit()
- self.Show(True)
-
- class MyApp(wxApp):
- def OnInit(self):
- wxInitAllImageHandlers()
- self.mainFrame = TestFrame()
- return True
-
- app = MyApp(0)
- app.MainLoop()
- worker.stop()
-
-